﻿using LightCAD.MathLib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static LightCAD.MathLib.Constants;

namespace LightCAD.Three
{
    public class WebGLMorphtargets
    {

        public static int numericalSort(ListEx<double> a, ListEx<double> b)
        {

            return Math.Sign(a[0] - b[0]);

        }

        public static int absNumericalSort(ListEx<double> a, ListEx<double> b)
        {
            return Math.Sign(Math.Abs(b[1]) - Math.Abs(a[1]));
        }

        public class Entry
        {
            public int count;
            public DataArrayTexture texture;
            public Vector2 size;

        };
        private WebGLCapabilities capabilities;
        private WebGLTextures textures;

        private JsObj<int, ListEx<ListEx<double>>> influencesList;
        private ListEx<double> morphInfluences;
        private JsObj<BufferGeometry, Entry> morphTextures;
        private Vector4 morph;
        private ListEx<ListEx<double>> workInfluences;
        public WebGLMorphtargets(WebGLCapabilities capabilities, WebGLTextures textures)
        {
            this.capabilities = capabilities;
            this.textures = textures;
            this.influencesList = new JsObj<int, ListEx<ListEx<double>>>();
            this.morphInfluences = new ListEx<double>(8);
            this.morphTextures = new JsObj<BufferGeometry, Entry>();
            this.morph = new Vector4();
            this.workInfluences = new ListEx<ListEx<double>>();
            for (var i = 0; i < 8; i++)
            {
                workInfluences[i] = new ListEx<double> { i, 0 };
            }

        }
        public void update(IMorphTargets _object, BufferGeometry geometry, WebGLProgram program)
        {

            var objectInfluences = _object.morphTargetInfluences;

            if (capabilities.isWebGL2)
            {

                // instead of using attributes, the WebGL 2 code path encodes morph targets
                // into an array of data textures. Each layer represents a single morph target.

                var morphAttribute = geometry.morphAttributes.position ?? geometry.morphAttributes.normal ?? geometry.morphAttributes.color;
                var morphTargetsCount = (morphAttribute != null) ? morphAttribute.Length : 0;

                var entry = morphTextures.get(geometry);

                if (entry == null || entry.count != morphTargetsCount)
                {

                    if (entry != null) entry.texture.dispose();

                    var hasMorphPosition = geometry.morphAttributes.position != null;
                    var hasMorphNormals = geometry.morphAttributes.normal != null;
                    var hasMorphColors = geometry.morphAttributes.color != null;

                    var morphTargets = geometry.morphAttributes.position ?? new ListEx<BufferAttribute>();
                    var morphNormals = geometry.morphAttributes.normal ?? new ListEx<BufferAttribute>();
                    var morphColors = geometry.morphAttributes.color ?? new ListEx<BufferAttribute>();

                    var vertexDataCount = 0;

                    if (hasMorphPosition) vertexDataCount = 1;
                    if (hasMorphNormals) vertexDataCount = 2;
                    if (hasMorphColors) vertexDataCount = 3;

                    var width = geometry.attributes.position.count * vertexDataCount;
                    var height = 1;

                    if (width > capabilities.maxTextureSize)
                    {

                        height = (int)Math.Ceiling(width / (double)capabilities.maxTextureSize);
                        width = capabilities.maxTextureSize;

                    }

                    var buffer = new double[width * height * 4 * morphTargetsCount];

                    var texture = new DataArrayTexture(buffer, width, height, morphTargetsCount);
                    texture.type = FloatType;
                    texture.needsUpdate = true;

                    // fill buffer

                    var vertexDataStride = vertexDataCount * 4;

                    for (var i = 0; i < morphTargetsCount; i++)
                    {

                        var morphTarget = morphTargets[i];
                        var morphNormal = morphNormals[i];
                        var morphColor = morphColors[i];

                        var offset = width * height * 4 * i;

                        for (var j = 0; j < morphTarget.count; j++)
                        {

                            var stride = j * vertexDataStride;

                            if (hasMorphPosition)
                            {

                                morph.FromBufferAttribute(morphTarget, j);

                                buffer[offset + stride + 0] = morph.X;
                                buffer[offset + stride + 1] = morph.Y;
                                buffer[offset + stride + 2] = morph.Z;
                                buffer[offset + stride + 3] = 0;

                            }

                            if (hasMorphNormals)
                            {

                                morph.FromBufferAttribute(morphNormal, j);

                                buffer[offset + stride + 4] = morph.X;
                                buffer[offset + stride + 5] = morph.Y;
                                buffer[offset + stride + 6] = morph.Z;
                                buffer[offset + stride + 7] = 0;

                            }

                            if (hasMorphColors)
                            {

                                morph.FromBufferAttribute(morphColor, j);

                                buffer[offset + stride + 8] = morph.X;
                                buffer[offset + stride + 9] = morph.Y;
                                buffer[offset + stride + 10] = morph.Z;
                                buffer[offset + stride + 11] = (morphColor.itemSize == 4) ? morph.W : 1;

                            }

                        }

                    }

                    entry = new Entry
                    {
                        count = morphTargetsCount,
                        texture = texture,
                        size = new Vector2(width, height)

                    };

                    morphTextures.set(geometry, entry);

                    void disposeTexture(EventArgs e)
                    {

                        texture.dispose();

                        morphTextures.delete(geometry);

                        geometry.removeEventListener("dispose", disposeTexture);

                    }

                    geometry.addEventListener("dispose", disposeTexture);

                }

                //

                var morphInfluencesSum = 0.0;

                for (var i = 0; i < objectInfluences.Length; i++)
                {

                    morphInfluencesSum += objectInfluences[i];

                }

                var morphBaseInfluence = geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;

                program.getUniforms().setValue("morphTargetBaseInfluence", morphBaseInfluence);
                program.getUniforms().setValue("morphTargetInfluences", objectInfluences);
                program.getUniforms().setValue("morphTargetsTexture", entry.texture, textures);
                program.getUniforms().setValue("morphTargetsTextureSize", entry.size);


            }
            else
            {

                // When object doesn"t have morph target influences defined, we treat it as a 0-length array
                // This is important to make sure we set up morphTargetBaseInfluence / morphTargetInfluences

                var length = objectInfluences == null ? 0 : objectInfluences.Length;

                var influences = influencesList[geometry.id];

                if (influences == null || influences.Length != length)
                {

                    // initialise list

                    influences = new ListEx<ListEx<double>>();

                    for (var i = 0; i < length; i++)
                    {

                        influences[i] = new ListEx<double> { i, 0 };

                    }

                    influencesList[geometry.id] = influences;

                }

                // Collect influences

                for (var i = 0; i < length; i++)
                {

                    var influence = influences[i];

                    influence[0] = i;
                    influence[1] = objectInfluences[i];

                }

                influences.Sort(absNumericalSort);

                for (var i = 0; i < 8; i++)
                {

                    if (i < length && influences[i][1] >= 0)
                    {

                        workInfluences[i][0] = influences[i][0];
                        workInfluences[i][1] = influences[i][1];

                    }
                    else
                    {

                        workInfluences[i][0] = int.MaxValue;
                        workInfluences[i][1] = 0;

                    }

                }

                workInfluences.Sort(numericalSort);

                var morphTargets = geometry.morphAttributes.position;
                var morphNormals = geometry.morphAttributes.normal;

                var morphInfluencesSum = 0.0;

                for (var i = 0; i < 8; i++)
                {

                    var influence = workInfluences[i];
                    var index = (int)influence[0];
                    var value = influence[1];

                    if (index != int.MaxValue && value >= 0)
                    {

                        if (morphTargets != null && geometry.getAttribute("morphTarget" + i) != morphTargets[index])
                        {

                            geometry.setAttribute("morphTarget" + i, morphTargets[index]);

                        }

                        if (morphNormals != null && geometry.getAttribute("morphNormal" + i) != morphNormals[index])
                        {

                            geometry.setAttribute("morphNormal" + i, morphNormals[index]);
                        }

                        morphInfluences[i] = value;
                        morphInfluencesSum += value;

                    }
                    else
                    {

                        if (morphTargets != null && geometry.hasAttribute("morphTarget" + i))
                        {

                            geometry.deleteAttribute("morphTarget" + i);

                        }

                        if (morphNormals != null && geometry.hasAttribute("morphNormal" + i))
                        {

                            geometry.deleteAttribute("morphNormal" + i);

                        }

                        morphInfluences[i] = 0;

                    }

                }

                // GLSL shader uses formula baseinfluence * base + sum(target * influence)
                // This allows us to switch between absolute morphs and relative morphs without changing shader code
                // When baseinfluence = 1 - sum(influence), the above is equivalent to sum((target - base) * influence)
                var morphBaseInfluence = geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;

                program.getUniforms().setValue("morphTargetBaseInfluence", morphBaseInfluence);
                program.getUniforms().setValue("morphTargetInfluences", morphInfluences);

            }

        }
    }
}
